In [1]:
%%HTML
<style type="text/css">
div.h1 {
    background-color:#3CB371; 
    color: white; 
    padding: 10px; 
    font-size: 35px; 
    max-width: 1500px; 
    margin: auto; 
}

div.h2 {
    background-color:#7B68EE; 
    color: white; 
    padding: 10px;
    font-size: 27px; 
    max-width: 1500px; 
    margin: auto; 
}

div.h3 {
    background-color:#FF6347; 
    color: white; 
    padding: 10px;
    font-size: 20px; 
    max-width: 1500px; 
    margin: auto; 
}
</style>

Assessing Individual Player Help Defense Performance in the 2018 NFL Season Passing Data

Consider the above play. This was a great moment for Wide Receiver Jamison Crowder, and a terrible moment for the defensive backs on the Giants. What went wrong? How did Crowder catch the ball at the 24 yard line, and run another 76 yards for a touchdown? It started with Rookie Cornerback Grant Haley, getting a bit crossed up as Crowder cut into the middle of the field, giving Crowder some space to get a clean catch. Then Haley missed the tackle. But, the play does not stop with the pass completion, nor with the missed tackle. Crowder ran another 70 yards after the missed tackle. Where was the rest of the team defense? Where was the help?

I introduce several novel player help defense metrics to measure the performance of players in situations like this, where the missed tackle, missed forced incompletion, and yards after completion do not tell the whole story of what was really happening with all the players on the defense during the passing play. In Crowder's case, every single defensive back on the field had an opportunity to stop Crowder prior to the 35 yard line if they had reacted optimally to the Quarterback's throw, whether they capitalized on it or not. Fortunately for Crowder, it was the latter.

Motivation

With the current defensive metrics, the defender who hustled to attempt the tackle and missed will be more penalized in his individual performance review than the defender who did not even bother to sprint to the receiver to attempt a chasedown tackle. The only reason is that there already exists an easy metric for capturing number of missed tackles, but the scope of quantifying the performance of a help defender is currently limited. However, computing metrics that quantify the difference in the help given and the help possible gives the opportunity to provide a more holistic picture of what impact a defender provides on the field.

To create metrics such as these, we need to establish the phases of a passing play and the roles of defenders during such a play. A passing play in the NFL can be divided into three primary phases: 1. Ball Snap -> Pass Forward; 2. Pass Forward -> Pass Arrived; 3. Pass Arrived -> Play End. The role of a defensive back evolves during a passing play, depending on whether the player they are responsible for guarding is the targeted receiver or not. The table below outlines the idealized defender roles during the phases of a passing play in the situations where they are guarding the targeted receiver versus where they are not.

Phase Guarding Targeted Receiver Not Guarding Targeted Receiver
1. Ball Snap -> Pass Forward Cover Assignment Cover Assignment
2. Pass Forward -> Pass Arrived Intercept Ball or Force Incompletion React to Help Cover Targeted Receiver
3. Pass Arrived -> Play End Stop Receiver ASAP Help Stop Receiver ASAP

Phases of the passing play and roles of defender depending on coverage assignment

Help Defense Performance Metrics

My help defense performance metrics are:

YATI: Yards After Timely Intersection

The number of yards that the help defender allows after the soonest possible moment that they could have reached the target receiver starting in Phase 2, Pass Forward -> Pass Arrived.

MOFI: Missed Opportunity to Force Incompletion

The number of missed opportunities in which the help defenders could have arrived before the pass arrives to the receiver.

HRT: Help Reaction Time

The time that it takes for the help defender to react and switch to targeting the player who has received the pass.

With the proposed help defense performance metrics, I can quantify the numer of yards that each defensive player gave up by not optimally moving to target Crowder in time and, well frankly, with enough chutzpah. Using this example, I will walk through how these metrics can be calculated and used to analyze the performance of an individual defender in switching from their receiver to the targeted receiver during Phases 2 (Pass Forward -> Pass Arrived) and 3 (Pass Arrived -> Play End) of the play.

To orient to the overhead view, I plot player and ball trajectories from Crowder's touchdown play below.

In [2]:
import matplotlib
import gc

import matplotlib.pyplot as plt
import numpy as np 
import pandas as pd

from matplotlib import animation, rc
from IPython.display import HTML

import field_py as field
from animateplay_py import AnimatePlay

import helper_py as h

# Animation for Crowder play
df_plays = pd.read_csv('../input/nfl-big-data-bowl-2021/plays.csv')
df_games = pd.read_csv('../input/nfl-big-data-bowl-2021/games.csv')
df_tracks = pd.read_csv('../input/nfl-big-data-bowl-2021/week14.csv')
games = df_tracks['gameId'].unique()
playId = 3577
gameId = 2018120908
play_df = h.slice_frame(df_tracks, playId, gameId)
play_df = df_tracks[df_tracks['playId'] == playId]
offense = play_df[play_df['position'] == 'QB']['team']
if 'away' in offense.values:
    defense = 'home'
else:
    defense = 'away'
anim = AnimatePlay(gameId, playId, df_tracks, defense)
# HTML(anim.anim.to_jshtml())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/opt/conda/lib/python3.7/site-packages/IPython/core/formatters.py in __call__(self, obj)
    339                 pass
    340             else:
--> 341                 return printer(obj)
    342             # Finally look for special method names
    343             method = get_real_method(obj, self.print_method)

/opt/conda/lib/python3.7/site-packages/IPython/core/pylabtools.py in <lambda>(fig)
    246 
    247     if 'png' in formats:
--> 248         png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs))
    249     if 'retina' in formats or 'png2x' in formats:
    250         png_formatter.for_type(Figure, lambda fig: retina_figure(fig, **kwargs))

/opt/conda/lib/python3.7/site-packages/IPython/core/pylabtools.py in print_figure(fig, fmt, bbox_inches, **kwargs)
    130         FigureCanvasBase(fig)
    131 
--> 132     fig.canvas.print_figure(bytes_io, **kw)
    133     data = bytes_io.getvalue()
    134     if fmt == 'svg':

/opt/conda/lib/python3.7/site-packages/matplotlib/backend_bases.py in print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, **kwargs)
   2080                     bbox_artists = kwargs.pop("bbox_extra_artists", None)
   2081                     bbox_inches = self.figure.get_tightbbox(renderer,
-> 2082                             bbox_extra_artists=bbox_artists)
   2083                     pad = kwargs.pop("pad_inches", None)
   2084                     if pad is None:

/opt/conda/lib/python3.7/site-packages/matplotlib/figure.py in get_tightbbox(self, renderer, bbox_extra_artists)
   2383 
   2384         for a in artists:
-> 2385             bbox = a.get_tightbbox(renderer)
   2386             if bbox is not None and (bbox.width != 0 or bbox.height != 0):
   2387                 bb.append(bbox)

/opt/conda/lib/python3.7/site-packages/matplotlib/axes/_base.py in get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists)
   4353                     # this artist
   4354                     continue
-> 4355             bbox = a.get_tightbbox(renderer)
   4356             if (bbox is not None
   4357                     and 0 < bbox.width < np.inf

/opt/conda/lib/python3.7/site-packages/matplotlib/artist.py in get_tightbbox(self, renderer)
    280             The enclosing bounding box (in figure pixel co-ordinates).
    281         """
--> 282         bbox = self.get_window_extent(renderer)
    283         if self.get_clip_on():
    284             clip_box = self.get_clip_box()

/opt/conda/lib/python3.7/site-packages/matplotlib/text.py in get_window_extent(self, renderer, dpi)
    893             self.figure.dpi = dpi
    894         if self.get_text() == '':
--> 895             tx, ty = self._get_xy_display()
    896             return Bbox.from_bounds(tx, ty, 0, 0)
    897 

/opt/conda/lib/python3.7/site-packages/matplotlib/text.py in _get_xy_display(self)
    225         Get the (possibly unit converted) transformed x, y in display coords.
    226         """
--> 227         x, y = self.get_unitless_position()
    228         return self.get_transform().transform((x, y))
    229 

/opt/conda/lib/python3.7/site-packages/matplotlib/text.py in get_unitless_position(self)
    826         # This will get the position with all unit information stripped away.
    827         # This is here for convenience since it is done in several locations.
--> 828         x = float(self.convert_xunits(self._x))
    829         y = float(self.convert_yunits(self._y))
    830         return x, y

TypeError: float() argument must be a string or a number, not 'list'
<Figure size 864x455.76 with 1 Axes>
In [3]:
anim
HTML(anim.anim.to_jshtml())
Out[3]:

YATI: Yards After Timely Intersection

In the Crowder play, it is obvious that the defense reacted late, and did not rally to make up for lost time to tackle him before the touchdown. But, how do we quantify that? I propose quantifying this by modeling the time-optimal trajectory for the defender to reach the receiver, and calculating the number of yards the receiver gained between that optimal intersection and the true takedown. To compute this, my input is the trajectory of the intended receiver from pass forward until play end and the position of the help defender at pass forward. At each $(x,y)$ location of the receiver's trajectory during this period, I compute the time-optimal time it would take the receiver to reach that point, assuming the receiver is starting at his location at pass forward. I compare that estimate with the actual time elapsed for the receiver to reach that same point on the field given his known trajectory. The earliest point on the receiver's known trajectory where the defense could have reached at or before the receiver (according to the time-optimal time) is what I determine to be the optimal intersection point. The number of yards the receiver gained from that point to takedown or play end is the YATI.

To compute the time-optimal time, I use the time-optimal trajectory model from (Burris, 2019). It estimates the time it takes for a player to arrive at a point that is $x$ yards away, running at $s$ speed, and turning throughout angle $a$. If the model predicts the defender is unable to intercept the receiver before the end of the play, then the time returned is the timestamp for the end of the play, thus the calculation will return 0 yards after optimal intersection. In running my algorithm on the Crowder play, I return the following number for each defender:

Defender YATI
Sean Chandler 74 yards
Janoris Jenkins 76 yards
B. W. Webb 63 yards
Alec Ogletree 75 yards
Kam Moore 68 yards
Tae Davis 74 yards
Olivier Vernon 74 yards

YATI Per Player for Crowder Touchdown Play

For reference, if I put Grant Haley's trajectory into the model, his YATI score is 71 yards allowed. Haley missed an attempted tackle at 70 yards out. Therefore, the model output aligns well with the intent of someone reacting to the targeted receiver at the moment of pass forward. This intuits that this metric can be used as an asymptotic lower bound for the soonest possible moment that a help defender could have reached the targeted receiver.

A graphical example of the algorithm's output is shown below using Sean Chandler's trajectory in the Crowder Touchdwn play. The connecting line (salmon colored) shows the time of pass forward on Chandler's trajectory and the time of Chandler's projected time-optimal intersection on Crowder's.

In [4]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import vis_py as vis

plt = vis.plot_yati_ex()
plt.show()

As evidenced by the graphic, the earliest possible moment Chandler could have reach Crowder is at the 26 yard line. Therefore, his YATI for this play would be recorded as 74 yards.

MOFI: Missed Opportunity to Force Incompletion

If the above computation returns an intersection that occurs before the moment of pass arrival, then it can be concluded that: had the defender reacted in time, he could have helped to force an incompletion. Thus, anytime the algorithm returns a time prior to pass arrival, that defender receives an uptick in this metric. In this case, Chandler could not have possibly reached Crowder in that time window, and would not be penalized. In this play, no help defenders (from this dataset at least) could have reached Crowder prior to the pass arrival.

HRT: Help Reaction Time

Reactions take time. As a defender, one cannot recognize who the Quarterback has targeted and react in 0.1 seconds to begin tracking the targeted receiver, which this metric uses as its lower bound. To see when the defender has moved in such a way to indicate they have begun tracking the targeted receiver, I look at how the optimal time of intersection changes throughout the defender's actual trajectory.

In particular, at time $t$, the defender is predicted to time-optimally reach the receiver at $T_{opt}^{(t)}$. At the next measured time, $t+\delta t$, the defender is predicted to time-optimally reach the receiver at $T_{opt}^{(t+\delta t)}$. The help reaction time is the minimum $t$ when

$$T_{opt}^{(t+\delta t)} - T_{opt}^{(t)} <= 0.01$$

In other words, it is the minimum time it takes for the help defender to begin consistently moving in a way that suggests they are actively tracking the targeted receiver. I filter out a small number of degenerate cases that display $T_{opt}^{(t)} = T_{opt}^{(t+\delta t)} >=$ time of play end.

The graphic below depicts the moments in which Chandler could have caught Crowder if he were moving optimally starting with the moment of pass forward. Note how the distances between intersection points on Crowder's trajectory increase as Chandler runs away from Crowder and decrease as he runs towards.

In [5]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import vis_py as vis

plt = vis.plot_rxn_ge()
plt.show()

Here, Chandler's reactions are shown throughout the play, the measurements at pass forward, which is the almost vertical line in the plot. It shows how Chandler did not start to move optimally (the slope flattens out) until well after the pass was ruled a catch. His HRT would be recorded as 7 seconds, an unusually long HRT, given the average length of an NFL play is 4 seconds long.

In [6]:
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
import vis_py as vis

plt = vis.plot_rxn_rate()
plt.show()

Chandler's change in time optimal intersections with Crowder throughout the play.

Analysis

All metrics are reported and plotted relative to league average. Positive indicates better than average. Player metrics are averaged over the 2018 NFL season.

Plotting these metrics gives insight into defensive styles of Safeties, Cornerbacks, and Linebackers. Below I plot the plus/minus average distance that a defensive player maintains from their target during Phase 1 (Ball Snap -> Pass Forward) of the play on the X axis. On the Y axis, I plot the average YATI that the athlete gives up per game. When just viewing the Cornerbacks, one notices that they cluster above the average for Coverage but the majority fall below average on YATI. Cornerbacks tend to pursue their targets more closely out of the gate. They also tend to maintain their trajectory in pursuit for well into Phase 2 (Pass Forward -> Pass Arrived) since they are often faced away from the Quarterback to maintain their speed to match their assignment. Meanwhile, Linebackers cluster around league average on coverage and the majority fall in the positive on YATI. Lastly, Safeties are all over the place. Their coverage is well below average. Given their position at the back of the field, this makes sense. Clicking on the position in the legend controls which position group is shown.

In [7]:
import vis_py as vis
import plotly.graph_objs as go

fig = vis.plot_cov_yati()
fig.show()

In comparing YATI vs HRT, the intent of the defender to move towards the target receiver can be better inferred. I observe that Safeties have the best HRT, Cornerbacks in the middle, and Linebackers at the bottom. Intuition about these positions realtive to the field of play aligns well with this. Safeties have room to react quickly and they are usually far away from the play, allowing them to see what the Quarterback is doing and react immediately to the pass. Meanwhile, the Linebackers are in the middle of everything and may be trying to blitz the quarterback or embroiled in some other task in which they must extricate themselves as they react to the Quarterback. The Cornerbacks are sometimes in traffic, sometimes not.

What is really interesting is the variance in YATI for a similar HRT score. Perhaps an athlete with a higher YATI relative to another with the same HRT is more effective at cutting through traffic when they decide to pursue the receiver, or perhaps they are less likely to miss a tackle when they arrive to help.

In [8]:
import vis_py as vis
import plotly.graph_objs as go

fig = vis.plot_yati_hrt()
fig.show()

Use Cases

These metrics have multiple possible use cases

  1. They can be computed for each defender to give a more holistic view of each defensive player, along with conventional metrics.

  2. Computing this on a weekly basis could allow teams to pick out plays in which a) defenders should have switched to the traget defender sooner or with more fervor and b) give a shout to out players who really stepped it up on help defense along with a quantifiable number attached to the recognition. This will give teams more to discuss in film reviews.

  3. In comparing an athlete's HRT relative to their YATI, the athletes ability to get to where they need to be versus their intent to get there can be evaluated more quantititatively.

For instance, I am sure that Luke Keuchly would loved to have been given a number that quantifies his awesomeness on the tackle below from 2018. Unfortunately, this play is a run and not included in this year's Data Bowl set, however, these metrics could quantify just how well he did to chase down and tackle Landry.

Future Development

The metrics I have introduced could be improved by taking into account more factors. They currently focus on the trajectories of one help defender and the target receiver. It does not take into account traffic on the field between the defender and the target receiver (which I do not have full access to in this dataset), nor does it consider if the defender did in fact reach the receiver, but missed the tackle, or was blocked by the receiver's teammate. Modeling the traffic, and plotting an optimal trajectory that takes into account potential collisions with other players would improve upon this metric. The intent of the player moving to the target vs their ability to actually stop the target can be even better teased apart. This would enable identifying which players do better in cutting through traffic to reach the receiver versus those who often find themselves bogged down. It could also be used to help identify who is particularly good at changing direction to track the receiver versus who gets their feet tripped up, or lastly who seems to react really quickly to change their course after the quarterback throws the ball versus those who have tunnel vision on their assignment for a tad bit too long.

References

Burris, K. (2019). A Trajectory Planning Algorithm for Quantifying Space Ownership in Professional Football. NFL Big Data Bowl.

My full code and results files can be can be found in this Github repository.